В данном проекте рассматривается задача анализа оттока пользователей интернет-магазина.
Основная цель работы заключается в выявлении причин оттока и анализе конкретных групп пользователей с наибольшим процентом прекращения использования данного сервиса.
Важной задачей является также поиск решения проблемы оттока пользователей, который будет проводиться путем рассмотрения наиболее интересных для анализа групп с целью выделения ключевых факторов, оказывающих наибольшее влияние на решение клиентов о завершении пользования магазином.
Решение будет протестировано с использованием симуляции и представлено в виде отчета и интерактивного дэшборда.
Загружаем нужные библиотеки для работы с данными, моделями и графиками.
library(DBI)
library(ggplot2)
library(plotly)
library(dplyr)
library(partykit)
library(caret)
library(tidymodels)
library(vip)
library(crosstalk)
Устанавливаем соединение с базой данных интернет-магазина.
con <- dbConnect(ClickHouseHTTP::ClickHouseHTTP(),
user='studentminor',
password='DataMinorHSE!2023',
dbname='ecommerce',
host='rc1a-i6ui9dhblsq8rgdo.mdb.yandexcloud.net',
port = 8443,
https=TRUE,
ssl_verifypeer=FALSE)
Рассмотрим структуру базы данных:
Список доступных таблиц:
dbListTables(con)
## [1] "category" "user" "useraccount"
Поля таблицы “useraccount”.
dbListFields(con, "useraccount")
## [1] "CustomerID" "Churn"
## [3] "Tenure" "PreferredLoginDevice"
## [5] "PreferredPaymentMode" "HourSpendOnApp"
## [7] "NumberOfDeviceRegistered" "SatisfactionScore"
## [9] "NumberOfAddress" "Complain"
## [11] "CouponUsed" "OrderCount"
## [13] "DaySinceLastOrder" "CashbackAmount"
## [15] "CategoryID"
Поля таблицы “category”.
dbListFields(con, "category")
## [1] "Column1" "CategoryID" "PreferedOrderCat"
Поля таблицы “user”.
dbListFields(con, "user")
## [1] "Column1" "CustomerID" "Gender" "MaritalStatus"
Посмотрим на распределение по оттоку в целом среди всех пользователей.
dataChurn = dbGetQuery(con, "select Churn from useraccount")
dataChurn = mutate(dataChurn, Churn = as.factor(Churn)) %>% count(Churn)
plot_ly(
data = dataChurn,
x = ~Churn,
y = ~n,
type = "bar",
marker = list(
color = ifelse(dataChurn$Churn == "1", "skyblue", "gray"),
line = list(color = 'black', width = 1)
)
) %>%
layout(
title = "Распределение по оттоку",
xaxis = list(title = "Клиент ушел", tickmode = "array", tickvals = c(0, 1), ticktext = c("Нет", "Да")),
yaxis = list(title = "Количество пользователей")
)
Из полученного графика можем заметить, что процент оттока в интернет-магазине равен ~16%.
Изучим статистику оттока по времени использования нашего магазина.
dataTenure = dbGetQuery(con, "select Churn, Tenure from useraccount")
dataTenure = mutate(dataTenure, Churn = as.factor(ifelse(Churn == '1', 'Yes', 'No'))) %>%
mutate(Tenure = as.numeric(Tenure)) %>%
na.omit() %>%
filter(Tenure < 40) # исключаем большие выбросы
ggplotly(
ggplot(dataTenure, aes(x = Tenure, fill = Churn)) +
geom_histogram(color = "black") +
labs(title = "Распределение по времени использования",
x = "Время использования (в месяцах)",
y = "Количество пользователей",
fill = "Ушёл") +
scale_fill_manual(values=c("gray", "skyblue"), labels = c("Нет", "Да")) +
theme_light()
)
Легко заметить, что подавляющее большинство пользователей уходит в самом начале пользования нашим магазином. Время использования у таких пользователей не превышает 1 месяц.
Чтобы лучше изучить проблему, рассмотрим эту группу подробнее и попытаемся понять почему люди перестают делать заказы через данный интернет-магазин.
Получим некоторую статистику новых и старых пользователей из базы данных.
dataLowTenure = dbGetQuery(con, "select Complain, OrderCount, CashbackAmount, CouponUsed from useraccount
where Tenure = '0' or Tenure = '1'")
dataHighTenure = dbGetQuery(con, "select Complain, OrderCount, CashbackAmount, CouponUsed from useraccount
where Tenure != '0' and Tenure != '1' and Tenure != ''")
Сравним средние значения некоторых показателей в каждой из групп:
paste("Новые пользователи =", round(mean(na.omit(as.numeric(dataLowTenure$Complain))), 3))
## [1] "Новые пользователи = 0.372"
paste("Старые пользователи =", round(mean(na.omit(as.numeric(dataHighTenure$Complain))), 3))
## [1] "Старые пользователи = 0.261"
paste("Новые пользователи =", round(mean(na.omit(as.numeric(dataLowTenure$OrderCount))), 3))
## [1] "Новые пользователи = 2.36"
paste("Старые пользователи =", round(mean(na.omit(as.numeric(dataHighTenure$OrderCount))), 3))
## [1] "Старые пользователи = 3.312"
paste("Новые пользователи =", round(mean(na.omit(as.numeric(dataLowTenure$CashbackAmount))), 3))
## [1] "Новые пользователи = 153.326"
paste("Старые пользователи =", round(mean(na.omit(as.numeric(dataHighTenure$CashbackAmount))), 3))
## [1] "Старые пользователи = 187.683"
paste("Новые пользователи =", round(mean(na.omit(as.numeric(dataLowTenure$CouponUsed))), 3))
## [1] "Новые пользователи = 1.466"
paste("Старые пользователи =", round(mean(na.omit(as.numeric(dataHighTenure$CouponUsed))), 3))
## [1] "Старые пользователи = 1.911"
Из выше приведенных данных видно, что у новых пользователей сильно меньше показатель среднего количества заказов, что весьма логично. Также они, в среднем, меньше используют промокоды и получают меньше кэшбэка. Но самое главное отличие - среди новых пользователей значительно выше процент жалоб. Это как раз и может вызывать такой сильный отток среди новых пользователей.
Остановимся на данном предположении и построим модель, которая будет предсказывать отток новых (Tenure <= 1) пользователей.
Для начала подготовим данные для построения модели, преобразовав типы колонок и убрав NA значения.
dataLowTenure = dbGetQuery(con, "select * from useraccount
where Tenure = '0' or Tenure = '1'")
dataLowTenure = dataLowTenure %>% mutate(Complain = as.factor(Complain),
Churn = as.factor(ifelse(Churn == '1', 'Yes', 'No')),
Tenure = as.numeric(Tenure),
PreferredLoginDevice = as.factor(PreferredLoginDevice),
PreferredPaymentMode = as.factor(PreferredPaymentMode),
HourSpendOnApp = as.numeric(HourSpendOnApp),
CouponUsed = as.numeric(CouponUsed),
OrderCount = as.numeric(OrderCount),
DaySinceLastOrder = as.numeric(DaySinceLastOrder)) %>%
na.omit()
Отключаемся от базы данных, она нам больше не пригодится в ходе дальнейшего поиска решения.
dbDisconnect(con)
Разобьем данные о новых пользователях на тренировочную и тестовую выборки с соотношением 80 на 20 соответственно.
set.seed(500)
ind = createDataPartition(dataLowTenure$Churn, p = 0.8, list = F)
train = dataLowTenure[ind,]
test = dataLowTenure[-ind,]
На всякий случай построим 2 разные модели и выберем лучшую из них. В моделях будем использовать формулу исключающую поля CustomerID, CategoryID и Tenure так как они не несут смысловой важности в текущем контексте.
Первая модель будет основана на дереве решений.
tree.model = ctree(Churn~. -CustomerID - CategoryID - Tenure, data = train)
Посчитаем аccuracy на тестовой выборке.
predTest = predict(tree.model, test, type="response")
confusionMatrix(predTest, test$Churn)
## Confusion Matrix and Statistics
##
## Reference
## Prediction No Yes
## No 72 24
## Yes 30 85
##
## Accuracy : 0.7441
## 95% CI : (0.6796, 0.8015)
## No Information Rate : 0.5166
## P-Value [Acc > NIR] : 1.064e-11
##
## Kappa : 0.4866
##
## Mcnemar's Test P-Value : 0.4962
##
## Sensitivity : 0.7059
## Specificity : 0.7798
## Pos Pred Value : 0.7500
## Neg Pred Value : 0.7391
## Prevalence : 0.4834
## Detection Rate : 0.3412
## Detection Prevalence : 0.4550
## Balanced Accuracy : 0.7428
##
## 'Positive' Class : No
##
Вторая модель будет основана на логистической регрессии.
logreg = logistic_reg() %>% fit(Churn~. - CustomerID - CategoryID - Tenure, data = train)
Посчитаем accuracy на тестовой выборке.
predlog = predict(logreg, test)
confusionMatrix(predlog$.pred_class, test$Churn)
## Confusion Matrix and Statistics
##
## Reference
## Prediction No Yes
## No 79 24
## Yes 23 85
##
## Accuracy : 0.7773
## 95% CI : (0.715, 0.8315)
## No Information Rate : 0.5166
## P-Value [Acc > NIR] : 4.936e-15
##
## Kappa : 0.5542
##
## Mcnemar's Test P-Value : 1
##
## Sensitivity : 0.7745
## Specificity : 0.7798
## Pos Pred Value : 0.7670
## Neg Pred Value : 0.7870
## Prevalence : 0.4834
## Detection Rate : 0.3744
## Detection Prevalence : 0.4882
## Balanced Accuracy : 0.7772
##
## 'Positive' Class : No
##
Как можно заметить, модель логистической регрессии определяет отток чуть точнее модели на дереве решений, поэтому будем использовать именно её в дальнейших предсказаниях.
Посмотрим на значимость переменных в выбранной модели.
imporatance = vi(logreg) %>% arrange(Importance)
plot_ly(alpha = 0.6) %>%
add_bars(y = ~Variable, x = ~Importance, data = imporatance,
name = "Varaibles", orientation = 'h',
marker = list(color='rgb(86, 180, 233)')) %>%
layout(barmode = "overlay",
yaxis = list(title = "Переменные",
categoryorder = "array",
categoryarray = ~n),
xaxis = list(title = "Важность в модели"))
Как можно увидеть из графика, переменная Complain, отвечающая за наличие жалобы у пользователей, действительно имеет большое значение в предсказании оттока.
Рассмотрим связь жалоб и ухода пользователей, построив график.
ggplotly(
ggplot(dataLowTenure, aes(x = Complain, fill = Churn)) +
geom_bar(color = "black") +
labs(title = "Отношение оттока и жалоб",
x = "Пользователь жаловался",
y = "Количество пользователей",
fill = "Ушёл") +
scale_fill_manual(values=c("gray", "skyblue"), labels = c("Нет", "Да")) +
scale_x_discrete(labels = c("Нет", "Да")) +
theme_light()
) %>%
layout(
showlegend = TRUE,
legend = list(
traceorder = "normal",
title = list(text = "Ушёл"),
orientation = "v",
y = 0.5,
xanchor = "left",
yanchor = "middle"
)
)
Можно заметить, что среди тех, кто оставлял жалобу в данном интернет-магазине, очень большое количество (~75%) перестало пользоваться сервисом. В то время как среди пользователей без жалоб процент оттока значительно меньше (~40%).
Попробуем сосредоточиться на тех, кто оставлял жалобы и в итоге ушёл.
Для решения проблемы оттока пользователей из-за жалоб интернет-магазин решил провести компанию по улучшению работы технической поддержки. Симулируем это, предположив, что улучшенная система поддержки будет лучше справляться с решением пользовательских проблем и будет выдавать в качестве извинения промокоды и повышенный кэшбэк.
Симуляция улучшения работы тех. поддержки будет заключатся в том, что с вероятностью 40% удалось решить пользовательскую проблему, а также выдать купоны и повысить кэшбэк.
Создадим копию тестового датасета.
test2 = test
Добавляем все пользователям купоны и увеличиваем кэшбэк независимо от того, получилось ли решить их проблему.
test2$CashbackAmount[test2$Complain == "1"] = as.integer(test2$CashbackAmount[test2$Complain == "1"] * 1.2)
test2$CouponUsed[test2$Complain == "1"] = test2$CouponUsed[test2$Complain == "1"] + 2
Симулируем попытку решения пользовательской проблемы с вероятность 40%.
test2$Complain[test2$Complain == "1"] =
sample(c("1", "0"),
size = length(test2$Complain[test2$Complain == "1"]),
replace = T, prob = c(0.6, 0.4))
Далее считаем предсказание на симуляции.
simChurn = predict(logreg, test2)$.pred_class
Визуализируем полученное предсказание, сравнивая со старыми значениями.
simChurn = data.frame(simChurn) %>% group_by(simChurn) %>%
summarise(n = length(simChurn)) %>%
mutate(title = simChurn, sim = "После")
curChurn = test %>% group_by(Churn) %>%
summarise(n = length(Churn)) %>%
mutate(title = Churn, sim = "До")
data = curChurn %>% bind_rows(simChurn) %>% select(-Churn, -simChurn) %>%
mutate(sim = as.factor(sim))
sharedData = SharedData$new(data)
bscols(
widths = c(3,NA),
filter <- filter_select("status",
"До или после симуляции",
sharedData,
~sim,
multiple = FALSE),
plot_ly(sharedData) %>%
add_bars(y = ~n, x = ~title, name = "После",
orientation = 'v',
marker = list(color = "green",
line = list(color = 'black', width = 1)),
opacity = 0.5,
transforms = list(list(type = "filter",
target = ~sim,
operation = "=",
value = "После"))) %>%
add_bars(y = ~n, x = ~title, name = "До",
orientation = 'v',
marker = list(color = "red",
line = list(color = 'black', width = 1)),
opacity = 0.5,
transforms = list(list(type = "filter",
target = ~sim,
operation = "=",
value = "До"))) %>%
layout(
barmode = "group",
yaxis = list(title = "Количество клиентов"),
xaxis = list(title = "Клиент ушёл"))
)
Как можно увидеть из графика, симулированной политикой нам удалось сохранить примерно 20-30% пользователей, которые собирались уходить с данной платформы.
В дэшборде, в первую очередь, представлена информация о симуляции решения проблемы. Графики нагляднее всего показывают эффективность представленного решения. Также в дэшборд добавлена иллюстрация процента пользователей, которых удалось сохранить, в виде числового значения.
В дополнение к информации о симуляции в дэшборд вынесен график, показывающий статистику использования интернет-магазина по времени с иллюстрацией ухода пользователей, и график, демонстрирующий соотношение жалоб среди новых пользователей и последующего ухода.
Вся выше перечисленная информация помогает наглядно понять смысл исследования и кратко проследить его ход. Полученный дэшборд можно использовать для демонстрации результатов анализа оттока пользователей начальству интернет-магазина.
В ходе проведенного анализа оттока пользователей интернет-магазина были выявлены несколько ключевых моментов.
Во-первых, процент оттока в представленном интернет-магазине составляет около 16%. При этом подавляющее большинство пользователей прекращают пользоваться сервисом либо почти сразу, либо спустя месяц после регистрации.
Во-вторых, в процессе анализа оттока новых клиентов выяснилось, что основной причиной их ухода является наличие жалоб. Около 75 процентов новых пользователей, оставивших жалобы за последний месяц, в итоге перестали пользоваться интернет-магазином.
Для решения проблемы оттока новых пользователей было выдвинуто предложение об усовершенствовании работы технической поддержки сайта. Его суть заключалась в улучшении способностей технической поддержки решать вопросы пользователей и предоставлять, в качестве извинения, промокоды и повышенный кэшбэк тем, кто столкнулся с проблемами.
После проведения симуляции данной компании удалось сохранить от 20 до 30 процентов (в зависимости от случайности разбиения) пользователей, которые собирались уходить.
Подводя итог, могу с уверенностью сказать, что данному интернет-магазину стоит сосредоточиться на привлечении и удержании новых пользователей, а также поработать над улучшением работы технической поддержки во избежание большого количества жалоб от пользователей.